/**
 * 
 */
package com.iss.card.mapv2;

import java.util.ArrayList;
import java.util.List;

import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.util.EntityUtils;

import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.v4.app.FragmentActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;

import com.google.android.gms.common.GooglePlayServicesNotAvailableException;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.GoogleMap.InfoWindowAdapter;
import com.google.android.gms.maps.GoogleMap.OnInfoWindowClickListener;
import com.google.android.gms.maps.MapsInitializer;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.android.gms.maps.model.PolylineOptions;
import com.iss.card.R;
import com.iss.card.common.ApplicationManager;
import com.iss.card.common.CommonValue;
import com.iss.card.model.StoreAddressInfo;

/**
 * @author dtdh
 * 
 */
public class GoogleMapView extends FragmentActivity {
	private final static String TAG = GoogleMapView.class.getSimpleName();

	private final static String GOOGLE_MAP_WEBAPI_DIRECTION_DRIVE = "http://maps.google.com/maps/api/directions/xml?&sensor=true&origin=%f,%f&destination=%f,%f";
	private final static String GOOGLE_MAP_WEBAPI_DIRECTION_WALK = "http://maps.google.com/maps/api/directions/xml?&mode=walking&sensor=true&origin=%f,%f&destination=%f,%f";

	private final class ENavigationMethod {
		public final static int ENMWalk = 0x10;
		public final static int ENMDrive = 0x11;
	}

	private final static int MSG_GOOGLE_MAP_INITIALIZE = 0x10;
	/**
	 * positions for those which will be displayed in map view as marker. the
	 * first one is the current position.
	 */
	private List<MarkerOptions> mPositions = new ArrayList<MarkerOptions>();

	/**
	 * navigation target.
	 */
	private MarkerOptions mNavigationTarget;

	/**
	 * Note that this may be null if the Google Play services APK is not
	 * available.
	 */
	private GoogleMap mMap;

	/** node for navigation line which is for walk. */
	private List<LatLng> mNavigationNodesByWalk = new ArrayList<LatLng>();
	/** node for navigation line which is for drive. */
	private List<LatLng> mNavigationNodesByDrive = new ArrayList<LatLng>();

	private Button mBTWalk;
	private Button mBTDrive;
	private Handler mHandler = new Handler() {
		public void handleMessage(Message msg) {
			switch (msg.what) {
			case MSG_GOOGLE_MAP_INITIALIZE:
				readData((Intent) msg.obj);
				break;

			default:
				break;
			}
		}
	};

	/**
	 * 
	 */
	public GoogleMapView() {
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see android.support.v4.app.FragmentActivity#onCreate(android.os.Bundle)
	 */
	@Override
	protected void onCreate(Bundle data) {
		super.onCreate(data);
		setContentView(R.layout.frame_google_map_view);

		setUpMapIfNeeded();
		mHandler.obtainMessage(MSG_GOOGLE_MAP_INITIALIZE, this.getIntent())
				.sendToTarget();
	}

	/**
	 * it is used to handle click event in [Return] button.
	 * 
	 * @param v
	 *            button of Return.
	 */
	public void onCancelMapView(View v) {
		Log.d(TAG, "onCancelMapView: v=" + v);
		finish();
	}

	/**
	 * it is used to handle click event in [Walk] button.
	 * 
	 * @param v
	 *            button of Walk.
	 */
	public void onNavigationByWalk(View v) {
		Log.d(TAG, "onNavigationByWalk: v=" + v);
		v.setSelected(true);
		mBTDrive.setSelected(false);
		showNavigationLine(ENavigationMethod.ENMWalk);
	}

	/**
	 * it is used to handle click event in [Drive] button.
	 * 
	 * @param v
	 */
	public void onNavigationByDrive(View v) {
		Log.d(TAG, "onNavigationByDrive: v=" + v);
		v.setSelected(true);
		mBTWalk.setSelected(false);
		showNavigationLine(ENavigationMethod.ENMDrive);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see android.support.v4.app.FragmentActivity#onDestroy()
	 */
	@Override
	protected void onDestroy() {
		super.onDestroy();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see android.support.v4.app.FragmentActivity#onLowMemory()
	 */
	@Override
	public void onLowMemory() {
		super.onLowMemory();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see android.support.v4.app.FragmentActivity#onPause()
	 */
	@Override
	protected void onPause() {
		super.onPause();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see android.support.v4.app.FragmentActivity#onResume()
	 */
	@Override
	protected void onResume() {
		super.onResume();
		setUpMapIfNeeded();
	}

	private void setPositions(List<StoreAddressInfo> list) {
		// step1. make sure google map have been initialized.
		try {
			MapsInitializer.initialize(this);
		} catch (GooglePlayServicesNotAvailableException e) {
			e.printStackTrace();
			return;
		}

		for (StoreAddressInfo info : list) {
			Log.d(TAG,
					"setPositions: " + info.getStoreLatitude() + ", "
							+ info.getStoreLongitude());
			MarkerOptions item = new MarkerOptions();
			item.position(new LatLng(info.getStoreLatitude(), info
					.getStoreLongitude()));
			item.title(info.getStoreName());
			item.snippet(info.getStoreAddress());
			item.icon(BitmapDescriptorFactory
					.fromResource(R.drawable.ballon_mark));
			mPositions.add(item);
		}
		addMarkers();
		centerMap();
	}

	/**
	 * initialize view according to view mode. if it is in EGMViewNavigation,
	 * navigation bar should be displayed. if it is in EGMViewRoundStore, title
	 * should be displayed.
	 * 
	 * @param viewMode
	 *            view mode. now it support EGMViewNavigation and
	 *            EGMViewRoundStore.
	 */
	private void initView(int viewMode) {
		LinearLayout navigation = (LinearLayout) this
				.findViewById(R.id.bt_google_map_view_navigation);
		TextView title = (TextView) this 
				.findViewById(R.id.tv_google_map_view_title);
		mBTDrive = (Button) this.findViewById(R.id.bt_google_map_view_driver);
		mBTWalk = (Button) this.findViewById(R.id.bt_google_map_view_walk);
		mBTWalk.setSelected(true);
		mBTDrive.setSelected(false);
		mBTDrive.setEnabled(false);
		mBTDrive.setEnabled(false);
		switch (viewMode) {
		case CommonValue.EnumGoogleMapviewMode.EGMViewNavigation:
			navigation.setVisibility(View.VISIBLE);
			title.setVisibility(View.GONE);
			break;
		case CommonValue.EnumGoogleMapviewMode.EGMViewRoundStore:
			title.setText(R.string.more_around_shop);
			navigation.setVisibility(View.GONE);
			title.setVisibility(View.VISIBLE);
			break;
		default:
			break;
		}
	}

	/**
	 * read data in intent which is from previous activity.
	 * 
	 * @param data
	 *            intent data include navigation data.
	 */
	private void readData(Intent data) {
		final int mapViewMode = data.getIntExtra(
				ApplicationManager.KEY_GMAP_MAP_VIEW_MODE,
				CommonValue.EnumGoogleMapviewMode.EGMViewNavigation);
		initView(mapViewMode);

		final List<StoreAddressInfo> positions = (List<StoreAddressInfo>) data
				.getSerializableExtra(ApplicationManager.KEY_GMAP_POSITION_DATA);
		setPositions(positions);
		if (mapViewMode == CommonValue.EnumGoogleMapviewMode.EGMViewNavigation) {
			if (mPositions.size() < 2) {
				return;
			}

			mNavigationTarget = mPositions.get(1);
			showNavigationLine(ENavigationMethod.ENMWalk);
		} else {
			if (mPositions.size() < 1) {
				return;
			}
			// set current position as default target.
			mNavigationTarget = mPositions.get(0);
			if (mMap != null) {
				mMap.setInfoWindowAdapter(new InfoWindowAdapter() {

					@Override
					public View getInfoContents(Marker v) {
						return null;
					}

					@Override
					public View getInfoWindow(Marker v) {
						return null;
					}

				});

				mMap.setOnInfoWindowClickListener(new OnInfoWindowClickListener() {

					@Override
					public void onInfoWindowClick(Marker v) {
						Log.d(TAG, "onInfoWindowClick: " + v);
						mNavigationNodesByDrive.clear();
						mNavigationTarget = new MarkerOptions();
						mNavigationTarget.position(v.getPosition());
						mNavigationTarget.title(v.getTitle());
						mNavigationTarget.snippet(v.getSnippet());
						showNavigationLine(ENavigationMethod.ENMDrive);
					}
				});
			}
		}
		
		mBTDrive.setEnabled(true);
		mBTWalk.setEnabled(true);
	}

	/**
	 * set map view in the center of all markers.
	 */
	private void centerMap() {
		if (mMap != null) {
			/**
			 * LatLng.latitude is in the range [-90, 90]. LatLng.longitude is in
			 * the range [-180, 180).
			 */
			double maxLat = -90;
			double maxLong = -180;
			double minLat = 90;
			double minLong = 180;
			for (MarkerOptions item : mPositions) {
				LatLng position = item.getPosition();
				maxLat = Math.max(maxLat, position.latitude);
				maxLong = Math.max(maxLong, position.longitude);
				minLat = Math.min(minLat, position.latitude);
				minLong = Math.min(minLong, position.longitude);
			}
			LatLng center = new LatLng((maxLat + minLat) / 2.0f,
					(maxLong + minLong) / 2.0f);
			Log.d(TAG, "centerMap:(" + center.latitude + ", "
					+ center.longitude + ")");
			/**
			 * public static CameraUpdate newLatLngZoom (LatLng latLng, float
			 * zoom) zoom: the desired zoom level, in the range of 2.0 to 21.0.
			 * Values below this range are set to 2.0, and values above it are
			 * set to 21.0. Increase the value to zoom in. Not all areas have
			 * tiles at the largest zoom levels.
			 */
			mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(center, 11));
		}
	}

	/**
	 * add markers in map view.
	 * 
	 * @return false means failed to add, true means success.
	 */
	private boolean addMarkers() {
		if (mMap != null) {
			for (MarkerOptions item : mPositions) {
				mMap.addMarker(item);
			}
			return true;
		}

		return false;
	}

	/**
	 * show navigation line according to navigation method. it includes walk and
	 * drive.
	 * 
	 * @param method
	 *            navigation method.
	 */
	private void showNavigationLine(int method) {
		switch (method) {
		case ENavigationMethod.ENMDrive:
			if (mNavigationNodesByDrive.size() == 0) {
				mNavigationNodesByDrive = getNavigationNodeByDrive();
			}

			addNavigationLine(mNavigationNodesByDrive);
			break;
		case ENavigationMethod.ENMWalk:
			if (mNavigationNodesByWalk.size() == 0) {
				mNavigationNodesByWalk = getNavigationNodeByWalk();
			}

			addNavigationLine(mNavigationNodesByWalk);
			break;
		default:
			break;
		}
		addMarkers();
	}

	private List<LatLng> getNavigationNodeByWalk() {
		mNavigationNodesByWalk.clear();
		LatLng home = mPositions.get(0).getPosition();
		final String url = String.format(GOOGLE_MAP_WEBAPI_DIRECTION_WALK,
				home.latitude, home.longitude,
				mNavigationTarget.getPosition().latitude,
				mNavigationTarget.getPosition().longitude);
		String result = requestNavigationLineFromGoogleServer(url);
		if (CommonValue.STR_EMPTY.equals(result)) {
			return new ArrayList<LatLng>();
		}
		Log.d(TAG, "getNavigationNodeByWalk: [" + result + "]");
		return decodePoly(result);
	}

	private List<LatLng> getNavigationNodeByDrive() {
		mNavigationNodesByDrive.clear();
		LatLng home = mPositions.get(0).getPosition();
		final String url = String.format(GOOGLE_MAP_WEBAPI_DIRECTION_DRIVE,
				home.latitude, home.longitude,
				mNavigationTarget.getPosition().latitude,
				mNavigationTarget.getPosition().longitude);
		String result = requestNavigationLineFromGoogleServer(url);
		if (CommonValue.STR_EMPTY.equals(result)) {
			return new ArrayList<LatLng>();
		}

		return decodePoly(result);
	}

	/**
	 * add navigation line into map view.
	 * 
	 * @param nodes
	 *            node set for navigation line.
	 */
	private void addNavigationLine(List<LatLng> nodes) {
		Log.d(TAG, "addNavigationLine");
		if (mMap != null) {
			mMap.clear(); // all markers and polyline are cleared.
			PolylineOptions line = new PolylineOptions();
			line.width(5);
			line.color(Color.BLUE);
			line.geodesic(true);
			for (LatLng node : nodes) {
				Log.d(TAG, "addNavigationLine:(" + node + ")");
				line.add(node);
			}
			mMap.addPolyline(line);
		}
	}

	/**
	 * request navigation line by google web api.
	 * 
	 * @param url
	 *            web api.
	 * @return navigation node.
	 */
	private String requestNavigationLineFromGoogleServer(String url) {
		Log.d(TAG, "requestNavigationLineFromGoogleServer: " + url);
		HttpGet get = new HttpGet(url);
		String strResult = "";
		try {
			HttpParams httpParameters = new BasicHttpParams();
			HttpConnectionParams.setConnectionTimeout(httpParameters, 3000);
			HttpClient httpClient = new DefaultHttpClient(httpParameters);

			HttpResponse httpResponse = null;
			httpResponse = httpClient.execute(get);

			if (httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
				strResult = EntityUtils.toString(httpResponse.getEntity());
			}
		} catch (Exception e) {
			return CommonValue.STR_EMPTY;
		}

		Log.d(TAG, "requestNavigationLineFromGoogleServer: (" + strResult + ")");
		if (-1 == strResult.indexOf("<status>OK</status>")) {
			Toast.makeText(this, "获取导航路线失败!", Toast.LENGTH_SHORT).show();
			return CommonValue.STR_EMPTY;
		}

		int pos = strResult.indexOf("<overview_polyline>");
		pos = strResult.indexOf("<points>", pos + 1);
		int pos2 = strResult.indexOf("</points>", pos);
		return strResult.substring(pos + 8, pos2);
	}

	/**
	 * decode navigation nodes which is from google web api with encoded.
	 * 
	 * @param encoded
	 *            encoded navigation line.
	 * @return
	 */
	private List<LatLng> decodePoly(String encoded) {
		List<LatLng> poly = new ArrayList<LatLng>();
		int index = 0, len = encoded.length();
		int lat = 0, lng = 0;
		while (index < len) {
			int b, shift = 0, result = 0;
			do {
				b = encoded.charAt(index++) - 63;
				result |= (b & 0x1f) << shift;
				shift += 5;
			} while (b >= 0x20);
			int dlat = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
			lat += dlat;
			shift = 0;
			result = 0;
			do {
				b = encoded.charAt(index++) - 63;
				result |= (b & 0x1f) << shift;
				shift += 5;
			} while (b >= 0x20);
			int dlng = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
			lng += dlng;
			LatLng position = new LatLng(lat * 1E-5, lng * 1E-5);
			poly.add(position);
		}
		return poly;
	}

	/**
	 * Sets up the map if it is possible to do so (i.e., the Google Play
	 * services APK is correctly installed) and the map has not already been
	 * instantiated.. This will ensure that we only ever call
	 * {@link #setUpMap()} once when {@link #mMap} is not null.
	 * <p>
	 * If it isn't installed {@link SupportMapFragment} (and
	 * {@link com.google.android.gms.maps.MapView MapView}) will show a prompt
	 * for the user to install/update the Google Play services APK on their
	 * device.
	 * <p>
	 * A user can return to this FragmentActivity after following the prompt and
	 * correctly installing/updating/enabling the Google Play services. Since
	 * the FragmentActivity may not have been completely destroyed during this
	 * process (it is likely that it would only be stopped or paused),
	 * {@link #onCreate(Bundle)} may not be called again so we should call this
	 * method in {@link #onResume()} to guarantee that it will be called.
	 */
	private void setUpMapIfNeeded() {
		// Do a null check to confirm that we have not already instantiated the
		// map.
		if (mMap == null) {
			// Try to obtain the map from the SupportMapFragment.
			mMap = ((SupportMapFragment) getSupportFragmentManager()
					.findFragmentById(R.id.map)).getMap();
		}
	}
}
